package br.net.woodstock.tickettoride.api.manager.common;

import java.io.ByteArrayOutputStream;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.security.auth.x500.X500Principal;

import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x500.style.IETFUtils;

import br.net.woodstock.rockframework.domain.service.ServiceException;
import br.net.woodstock.rockframework.persistence.orm.QueryMetadata;
import br.net.woodstock.rockframework.security.cert.CertificateBuilderRequest;
import br.net.woodstock.rockframework.security.cert.CertificateType;
import br.net.woodstock.rockframework.security.cert.KeyUsageType;
import br.net.woodstock.rockframework.security.cert.PrivateKeyHolder;
import br.net.woodstock.rockframework.security.cert.impl.BouncyCastleCertificateBuilder;
import br.net.woodstock.rockframework.security.store.KeyStoreType;
import br.net.woodstock.rockframework.security.store.PasswordAlias;
import br.net.woodstock.rockframework.security.store.PrivateKeyEntry;
import br.net.woodstock.rockframework.security.store.Store;
import br.net.woodstock.rockframework.security.store.impl.JCAStore;
import br.net.woodstock.rockframework.security.util.SecurityUtils;
import br.net.woodstock.tickettoride.api.core.common.AbstractCommonService;
import br.net.woodstock.tickettoride.api.core.utils.RepositoryHelper;
import br.net.woodstock.tickettoride.api.manager.CertificateRequest;
import br.net.woodstock.tickettoride.api.manager.CertificateService;
import br.net.woodstock.tickettoride.client.utils.CodecUtils;
import br.net.woodstock.tickettoride.orm.Application;
import br.net.woodstock.tickettoride.orm.Certificate;
import br.net.woodstock.tickettoride.orm.CertificateStatus;

public abstract class AbstractCertificateService extends AbstractCommonService implements CertificateService {

	private static final long				serialVersionUID						= -1331892934800753324L;

	private static final String				JPQL_DISABLE_CERTIFICATE_BY_APPLICATION	= "UPDATE Certificate SET status = 1 WHERE application.id = :id";

	private static final String				JPQL_GET_CERTIFICATE_BY_APPLICATION		= "SELECT c FROM Certificate AS c JOIN c.application AS a WHERE a.id = :id AND c.status = 0";

	private static final String				JPQL_LIST_CERTIFICATE_BY_APPLICATION	= "SELECT c FROM Certificate AS c JOIN c.application AS a WHERE a.id = :id";

	private BouncyCastleCertificateBuilder	builder									= BouncyCastleCertificateBuilder.getInstance();

	@Override
	public Certificate getById(final Integer id) {
		try {
			return this.getSimpleRepository().get(new Certificate(id));
		} catch (Exception e) {
			throw new ServiceException(e);
		}
	}

	@Override
	public Certificate getByApplication(final Integer applicationId) {
		try {
			Map<String, Object> parameters = new HashMap<String, Object>();
			parameters.put("id", applicationId);
			QueryMetadata metadata = RepositoryHelper.toQueryMetadata(AbstractCertificateService.JPQL_GET_CERTIFICATE_BY_APPLICATION, parameters);
			return this.getSimpleQueryableRepository().getSingle(metadata);
		} catch (Exception e) {
			throw new ServiceException(e);
		}
	}

	@Override
	public Integer create(final CertificateRequest request) {
		try {
			CertificateBuilderRequest cbr = this.getCertificateBuilderRequest(request.getSubject(), request.getIssuer());
			PrivateKeyHolder holder = this.builder.build(cbr);
			X509Certificate x509Certificate = (X509Certificate) holder.getChain()[0];
			Store store = new JCAStore(KeyStoreType.PKCS12);
			store.add(new PrivateKeyEntry(new PasswordAlias(request.getAlias(), request.getKeyPassword()), holder.getPrivateKey(), holder.getChain()));

			ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
			store.write(outputStream, request.getStorePassword());
			byte[] pkcs12 = CodecUtils.toBase64(outputStream.toByteArray());

			byte[] data = CodecUtils.toBase64(x509Certificate.getEncoded());
			String hash = CodecUtils.getSHA1String(x509Certificate.getEncoded());
			Date notAfter = x509Certificate.getNotAfter();
			String serialNumber = x509Certificate.getSerialNumber().toString();

			Map<String, Object> parameters = new HashMap<String, Object>();
			parameters.put("id", request.getApplicationId());
			QueryMetadata metadata = RepositoryHelper.toQueryMetadata(AbstractCertificateService.JPQL_DISABLE_CERTIFICATE_BY_APPLICATION, parameters);
			this.getSimpleQueryableRepository().executeUpdate(metadata);

			Certificate certificate = new Certificate();
			certificate.setAlgorithm(x509Certificate.getSigAlgName());
			certificate.setAlias(request.getAlias());
			certificate.setApplication(new Application(request.getApplicationId()));
			certificate.setData(data);
			certificate.setHash(hash);
			certificate.setIssuer(request.getIssuer());
			certificate.setNotAfter(notAfter);
			certificate.setPkcs12(pkcs12);
			certificate.setSerialNumber(serialNumber);
			certificate.setSubject(request.getSubject());
			certificate.setStatus(CertificateStatus.ACTIVE);
			this.getSimpleRepository().save(certificate);

			return certificate.getId();
		} catch (Exception e) {
			throw new ServiceException(e);
		}
	}

	@Override
	public Integer upload(final Integer applicationId, final String alias, final byte[] bytes) {
		try {
			X509Certificate x509Certificate = (X509Certificate) SecurityUtils.getCertificateFromFile(bytes, CertificateType.X509);

			byte[] data = CodecUtils.toBase64(x509Certificate.getEncoded());
			String hash = CodecUtils.getSHA1String(x509Certificate.getEncoded());
			Date notAfter = x509Certificate.getNotAfter();
			String serialNumber = x509Certificate.getSerialNumber().toString();
			String subject = this.getValue(x509Certificate.getSubjectX500Principal());
			String issuer = this.getValue(x509Certificate.getIssuerX500Principal());

			Map<String, Object> parameters = new HashMap<String, Object>();
			parameters.put("id", applicationId);
			QueryMetadata metadata = RepositoryHelper.toQueryMetadata(AbstractCertificateService.JPQL_DISABLE_CERTIFICATE_BY_APPLICATION, parameters);
			this.getSimpleQueryableRepository().executeUpdate(metadata);

			Certificate certificate = new Certificate();
			certificate.setAlgorithm(x509Certificate.getSigAlgName());
			certificate.setAlias(alias);
			certificate.setApplication(new Application(applicationId));
			certificate.setData(data);
			certificate.setHash(hash);
			certificate.setIssuer(issuer);
			certificate.setNotAfter(notAfter);
			certificate.setPkcs12(null);
			certificate.setSerialNumber(serialNumber);
			certificate.setSubject(subject);
			certificate.setStatus(CertificateStatus.ACTIVE);
			this.getSimpleRepository().save(certificate);

			return certificate.getId();
		} catch (Exception e) {
			throw new ServiceException(e);
		}
	}

	@Override
	public Collection<Certificate> listByApplication(final Integer applicationId) {
		try {
			Map<String, Object> parameters = new HashMap<String, Object>();
			parameters.put("id", applicationId);
			QueryMetadata metadata = RepositoryHelper.toQueryMetadata(AbstractCertificateService.JPQL_LIST_CERTIFICATE_BY_APPLICATION, parameters);
			return this.getSimpleQueryableRepository().getCollection(metadata).getResult();
		} catch (Exception e) {
			throw new ServiceException(e);
		}
	}

	private CertificateBuilderRequest getCertificateBuilderRequest(final String subject, final String issuer) {
		CertificateBuilderRequest request = new CertificateBuilderRequest(subject, issuer);
		request.withKeyUsage(KeyUsageType.DIGITAL_SIGNATURE, KeyUsageType.NON_REPUDIATION, KeyUsageType.KEY_AGREEMENT);
		return request;
	}

	protected String getValue(final X500Principal principal) {
		X500Name x500Name = new X500Name(principal.getName());
		RDN rdn = x500Name.getRDNs(BCStyle.CN)[0];
		return IETFUtils.valueToString(rdn.getFirst().getValue());
	}

}
